"""Pipeline herder main module."""
import argparse
import dataclasses
import datetime
import os
import sys

from cki_lib import messagequeue
from cki_lib import metrics
from cki_lib import misc
from cki_lib.logger import get_logger
import dateutil
import prometheus_client as prometheus
import sentry_sdk

from . import utils

LOGGER = get_logger('cki_tools.pipeline_herder.main')

METRIC_MESSAGE_DELAYED = prometheus.Counter(
    'cki_message_delayed', 'Number of queue messages delayed via retry queue')
METRIC_PROBLEM_DETECTED = prometheus.Counter(
    'cki_herder_problem_detected', 'Number of problems detected',
    ['gitlab_stage', 'gitlab_job', 'matcher'])
METRIC_PROBLEM_RETRIES = prometheus.Histogram(
    'cki_herder_problem_retries', 'Number of retries for a job with a problem',
    ['gitlab_stage', 'gitlab_job', 'matcher'])
METRIC_NO_PROBLEM_DETECTED = prometheus.Counter(
    'cki_herder_no_problem_detected', 'Number of jobs without a detected problem',
    ['gitlab_stage', 'gitlab_job'])
METRIC_PROBLEM_REPORTED = prometheus.Counter(
    'cki_herder_problem_reported', 'Number of problems reported',
    ['gitlab_stage', 'gitlab_job', 'matcher'])
METRIC_PROBLEM_RETRIED = prometheus.Counter(
    'cki_herder_problem_retried', 'Number of problems retried',
    ['gitlab_stage', 'gitlab_job', 'matcher'])
METRIC_PROCESS_TIME = prometheus.Histogram(
    'cki_herder_process_time_seconds', 'Time spent matching a job')


def process_webhook(body=None, headers=None, ack_fn=None, **_):
    """Process a job from a GitLab webhook."""
    match headers.get('message-type'), headers.get('message-herder-type'), body.get('object_kind'):
        case 'gitlab', _, 'build':
            result = handle_build(body)
        case 'gitlab', _, object_kind:
            LOGGER.info('Ignoring gitlab.%s: %s', object_kind, body.get('project_name', 'unknown'))
            result = True
        case 'herder', 'retry', _:
            result = handle_retry_trigger(body)
        case 'herder', herder_type, _:
            LOGGER.error('Ignoring unexpected herder.%s', herder_type)
            result = True
        case message_type, _, _:
            LOGGER.error('Ignoring unexpected %s', message_type)
            result = True
    if not result:
        METRIC_MESSAGE_DELAYED.inc()
    ack_fn(result)


def handle_retry_trigger(message):
    """Handle a pipeline-herder retry message."""
    if datetime.datetime.now().astimezone() < dateutil.parser.parse(message['not_before']):
        return False
    retry(message['web_url'], utils.MatchResult(**message['result']))
    return True


def handle_build(message):
    """Handle a GitLab job webhook."""
    status = message['build_status']
    if status in ('success', 'failed'):
        process_job(f'{message["project"]["web_url"]}/-/jobs/{message["build_id"]}')
    else:
        LOGGER.info('Ignoring %s for %s', status, message["project_name"])
    return True


@METRIC_PROCESS_TIME.time()
def process_job(web_url):
    """Process a job directly specified via host/project/id."""
    job = utils.CachedJob(web_url)
    job_id = job.gl_job.id
    LOGGER.info('Processing P%s J%s', job.gl_pipeline.id, job_id)

    match_result = utils.check(job)

    if not match_result or match_result.action == 'report':
        # Send message about finished job.
        notify_finished(job.gl_job.web_url)

    if not match_result:
        # No problem found, nothing else to do.
        METRIC_NO_PROBLEM_DETECTED.labels(
            gitlab_stage=job.gl_job.stage,
            gitlab_job=job.gl_job.name,
        ).inc()
        return None

    description = match_result.description

    METRIC_PROBLEM_DETECTED.labels(
        gitlab_stage=job.gl_job.stage,
        gitlab_job=job.gl_job.name,
        matcher=match_result.name,
    ).inc()
    METRIC_PROBLEM_RETRIES.labels(
        gitlab_stage=job.gl_job.stage,
        gitlab_job=job.gl_job.name,
        matcher=match_result.name,
    ).observe(
        job.job_name_count()
    )

    if match_result.action in {'report', 'alert'}:
        utils.notify(job, f'Detected {description}', True)
        METRIC_PROBLEM_REPORTED.labels(
            gitlab_stage=job.gl_job.stage,
            gitlab_job=job.gl_job.name,
            matcher=match_result.name,
        ).inc()
        return match_result.action

    if match_result.action == 'retry':
        retry_rating = job.retry_rating(match_result)
        if not retry_rating.reason.retry:
            utils.notify(job,
                         f'Detected {description}, not retrying: {retry_rating.message}',
                         retry_rating.reason.notify_chat)
            METRIC_PROBLEM_REPORTED.labels(
                gitlab_stage=job.gl_job.stage,
                gitlab_job=job.gl_job.name,
                matcher=match_result.name,
            ).inc()
            return 'alert'
        if not (delay := job.retry_delay(match_result)):
            utils.notify(job, f'Detected {description}, retrying now',
                         retry_rating.reason.notify_chat)
        else:
            utils.notify(job, f'Detected {description}, retrying in {delay} minutes',
                         retry_rating.reason.notify_chat)
        submit_retry(job.gl_job.web_url, match_result, delay)
        METRIC_PROBLEM_RETRIED.labels(
            gitlab_stage=job.gl_job.stage,
            gitlab_job=job.gl_job.name,
            matcher=match_result.name,
        ).inc()
        return match_result.action

    utils.notify(job,
                 f'Detected {description}. 🔥 Action {match_result.action} not handled 🔥',
                 True)
    return 'error'


def notify_finished(web_url):
    """Add finished job to the message queue."""
    if not misc.is_production_or_staging():
        LOGGER.info('Not notifying via AMQP because of development environment')
        return
    messagequeue.MessageQueue().send_message(
        {'web_url': web_url},
        'herder.build',
        exchange=os.environ.get('PIPELINE_HERDER_PUBLISH_EXCHANGE'),
        headers={'message-type': 'herder', 'message-herder-type': 'build'},
    )


def submit_retry(web_url, match_result: utils.MatchResult, delay: datetime.timedelta):
    """Submit the suggested actions with the specified delay in minutes."""
    if not misc.is_production():
        LOGGER.info('Not retrying because of non-production environment')
        return
    not_before = datetime.datetime.now().astimezone() + delay
    messagequeue.MessageQueue().send_message(
        {
            'web_url': web_url,
            'not_before': not_before.isoformat(),
            'result': dataclasses.asdict(match_result),
        },
        'herder.retry',
        exchange=os.environ.get('PIPELINE_HERDER_PUBLISH_EXCHANGE'),
        headers={'message-type': 'herder', 'message-herder-type': 'retry'},
    )


def retry(web_url, match_result: utils.MatchResult):
    """Safely retry a job."""
    job = utils.CachedJob(web_url)
    retry_rating = job.retry_rating(match_result)
    if not retry_rating.reason.retry:
        utils.notify(job, f'Not retried: {retry_rating.message}', retry_rating.reason.notify_chat)
    else:
        if job.gl_job.status == 'running':
            job.gl_job.cancel()
        job.gl_job.retry()
        utils.notify(job, 'Retried', retry_rating.reason.notify_chat)


def process_queue() -> None:
    """Process jobs from the message queue."""
    metrics.prometheus_init()
    messagequeue.MessageQueue().consume_messages(
        os.environ.get('WEBHOOK_RECEIVER_EXCHANGE', 'cki.exchange.webhooks'),
        os.environ.get('PIPELINE_HERDER_ROUTING_KEYS', '').split(),
        process_webhook,
        queue_name=os.environ.get('PIPELINE_HERDER_QUEUE'),
        manual_ack=True)


def process_exemplars() -> int:
    """Process all embedded exemplars."""
    if (invalid := [
        j for m in utils.match_contexts() for j in m.exemplars
        if not utils.Matcher(m, utils.CachedJob(j)).check()
    ]):
        print(f'Unable to validate exemplars: {invalid}')
        return 1
    return 0


def process_single(job_url: str) -> int:
    """Process a single job from the command line."""
    if match_result := utils.check(utils.CachedJob(job_url)):
        print(match_result.description)
        return 0
    print('Nothing matched 😕')
    return 1


def main(argv: list[str] | None = None) -> int:
    """CLI Interface."""
    parser = argparse.ArgumentParser()
    parser.add_argument('--job-url',
                        help='Try the matcher for an individual job.')
    parser.add_argument('--validate', action='store_true',
                        help='Validate configuration via embeddded job exemplars')
    args = parser.parse_args(argv)

    if args.validate:
        return process_exemplars()
    if args.job_url:
        return process_single(args.job_url)

    process_queue()
    return 0


if __name__ == '__main__':
    misc.sentry_init(sentry_sdk)
    sys.exit(main())
